'use strict'

var Promise = require('bluebird');
// 引入 request 并且 Promise 化，下面的 updateAccessToken 里面，request() 后面才能跟 then();
// 只能对尚未支持 promise 的函数进行 promise 化。
var request = Promise.promisify(require('request'));
var _ = require('lodash');
var fs = require('fs');
var util = require('./util');

// 配置项
var prefix = 'https://api.weixin.qq.com/cgi-bin/';

// 二维码
var mpPrefix = 'https://mp.weixin.qq.com/cgi-bin/'; // showqrcode?ticket=TICKET

// 语义理解（语音转文字，这个接口在哪里？）认证的服务号
var semanticUrl = 'https://api.weixin.qq.com/semantic/search?'; // access_token=ACCESS_TOKEN

var api = {
	// 获取 access_token 的接口
	accessToken: prefix + 'token?grant_type=client_credential', // &appid=APPID&secret=APPSECRET
	// 上传、下载临时素材的接口
	temporary: {
		upload: prefix + 'media/upload?', // ?access_token=xxx&type=xxx
		
		// http请求方式: GET,https调用。
		// 视频文件不支持https下载，调用该接口需http协议。
		fetch: prefix + 'media/get?'// access_token=ACCESS_TOKEN&media_id=MEDIA_ID
	},
	// 上传、下载、删除永久素材的接口
	permanent: {
		// 其他永久素材
		upload: prefix + 'material/add_material?', // ?access_token=ACCESS_TOKEN&type=TYPE
		// 图文素材
		uploadNews: prefix + 'material/add_news?', // ?access_token=xxx
		// 图文素材需要的图片的url
		// 本接口所上传的图片不占用公众号的素材库中图片数量的5000个的限制。图片仅支持jpg/png格式，大小必须在1MB以下。
		uploadNewsPic: prefix + 'media/uploadimg?', // ?access_token=xxx
		// 特别注意：这里是 media，不是 material。
		
		// http请求方式: POST,https协议
		fetch: prefix + 'material/get_material?', // access_token=ACCESS_TOKEN
		// {"media_id":MEDIA_ID}
		
		// 删除
		del: prefix + 'material/del_material?', // access_token=ACCESS_TOKEN
		// http请求方式: POST
		// {"media_id":MEDIA_ID}
		
		// 修改永久素材
		update: prefix + 'material/update_news?', // access_token=ACCESS_TOKEN
		// http请求方式: POST
		/*
{
 "media_id":MEDIA_ID,
 "index":INDEX,
 "articles": {
      "title": TITLE,
      "thumb_media_id": THUMB_MEDIA_ID,
      "author": AUTHOR,
      "digest": DIGEST,
      "show_cover_pic": SHOW_COVER_PIC(0 / 1),
      "content": CONTENT,
      "content_source_url": CONTENT_SOURCE_URL
   }
}
		*/
		
		// 获取永久素材数
		count: prefix + 'material/get_materialcount?', // access_token=ACCESS_TOKEN
		// http请求方式: GET
		
		// 获取永久素材列表（无法获取临时素材）
		batch: prefix + 'material/batchget_material?', // access_token=ACCESS_TOKEN 
		// http请求方式: POST
		/*
{
   "type":TYPE,
   "offset":OFFSET,
   "count":COUNT
}		
		*/
	},
	
	// 用户分组（貌似现在已经没有该功能了，变成了用户标签）
	// 组名可以重复，id不会。
	// curd有个缓存问题，可能不会立即生效，需要注意。
	group: {
		create: prefix + 'groups/create?', // access_token=ACCESS_TOKEN 
		fetch: prefix + 'groups/get?', // access_token=ACCESS_TOKEN 
		check: prefix + 'groups/getid?', // access_token=ACCESS_TOKEN 
		update: prefix + 'groups/update?', // access_token=ACCESS_TOKEN 
		move: prefix + 'groups/members/update?', // access_token=ACCESS_TOKEN 
		batchmove: prefix + 'groups/members/batchupdate?', // access_token=ACCESS_TOKEN 
		del: prefix + 'groups/delete?', // access_token=ACCESS_TOKEN 
	},
	
	// 用户信息操作
	user: {
		// 设置备注名，只对微信认证的服务号开放该接口
		remark: prefix + 'user/info/updateremark?', // access_token=ACCESS_TOKEN 
		// 获取单个用户信息
		fetch: prefix + 'user/info?', // access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN 
		// 批量获取用户信息
		batchFetch: prefix + 'user/info/batchget?', // access_token=ACCESS_TOKEN 
		// 获取用户列表，一次最多1万个
		list: prefix + 'user/get?', // access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID
	},
	
	// 群发消息
	mass: {
		// 根据分组进行群发（现在是根据标签进行群发了，接口一致）
		group: prefix + 'message/mass/sendall?', // access_token=ACCESS_TOKEN 
		// 根据OpenID列表群发【订阅号不可用，服务号认证后可用】
		openid: prefix + 'message/mass/send?', // access_token=ACCESS_TOKEN 
		// 删除群发【订阅号与服务号认证后均可用】
		// 群发之后，随时可以通过该接口删除群发。之前是半个小时内可以删除现在没有这个说明了。
		del: prefix + 'message/mass/delete?', // access_token=ACCESS_TOKEN 
		// 预览接口【订阅号与服务号认证后均可用】
		preview: prefix + 'message/mass/preview?', // access_token=ACCESS_TOKEN 
		// 查询群发消息发送状态【订阅号与服务号认证后均可用】
		check: prefix + 'message/mass/get?'// access_token=ACCESS_TOKEN 
	},
	
	menu: {
		// 创建通用（自定义）菜单
		create: prefix + 'menu/create?', // access_token=ACCESS_TOKEN 
		// 获取通用（自定义）菜单【在设置了个性化菜单后，使用本自定义菜单查询接口可以获取默认菜单和全部个性化菜单信息。】
		fetch: prefix + 'menu/get?', // access_token=ACCESS_TOKEN 
		// 删除通用（自定义）菜单，如果存在个性化菜单，会一起删除（此接口会删除默认菜单及全部个性化菜单。）
		del: prefix + 'menu/delete?', // access_token=ACCESS_TOKEN 
		// 获取通用（自定义）菜单配置接口
		// 【本接口将会提供公众号当前使用的自定义菜单的配置，如果公众号是通过API调用设置的菜单，则返回菜单的开发配置；
		// 而如果公众号是在公众平台官网通过网站功能发布菜单，则本接口返回运营者设置的菜单配置。】
		current: prefix + 'get_current_selfmenu_info?', // access_token=ACCESS_TOKEN
		// 创建个性化菜单
		addconditional: prefix + 'menu/addconditional?', // access_token=ACCESS_TOKEN
		// 删除个性化菜单
		addconditional: prefix + 'menu/delconditional?', // access_token=ACCESS_TOKEN
		// 查询个性化菜单（在获取通用（自定义）菜单时，如果存在个性化菜单，会一起返回）
	},
	
	// 二维码
	qrcode: {
		// 临时和永久二维码（post数据不同）
		// 会返回 ticket，用以换取二维码图片
		create: prefix + 'qrcode/create?', // access_token=TOKEN
		/*
临时二维码格式
POST数据格式：json
POST数据例子：
{"expire_seconds": 604800, "action_name": "QR_SCENE", "action_info": {"scene": {"scene_id": 123}}}
或者也可以使用以下POST数据创建字符串形式的二维码参数：
{"expire_seconds": 604800, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}
		
永久二维码格式
POST数据格式：json
POST数据例子：
{"action_name": "QR_LIMIT_SCENE", "action_info": {"scene": {"scene_id": 123}}}
或者也可以使用以下POST数据创建字符串形式的二维码参数：
{"action_name": "QR_LIMIT_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}
		*/
		
		// 通过 ticket 换取二维码
		// 获取二维码ticket后，开发者可用ticket换取二维码图片。请注意，本接口无须登录态即可调用。
		show: mpPrefix + 'showqrcode?', // ticket=TICKET （TICKET记得进行UrlEncode）
	},
	
	// 长链接转短链接
	shortUrl: {
		create: prefix + 'shorturl?', // access_token=ACCESS_TOKEN 
	},
	
	// 语义接口
	semanticUrl: semanticUrl,
};

/**
 * 处理 access_token
 * @param opts app.js中config对象的wechat属性值
 * @constructor
 */
function Wechat(opts)
{
	var that = this;
	this.appID = opts.appID;
	this.appSecret = opts.appSecret;

	// 下面两个方法，都是 Promise 对象，所以后面能跟 then 方法。
	this.getAccessToken = opts.getAccessToken;
	this.saveAccessToken = opts.saveAccessToken;

	this.fetchAccessToken();
}

// 原型链上添加方法
Wechat.prototype.fetchAccessToken = function(){
	var that = this;
	if (this.access_token && this.expires_in) {
		if (this.isValidAccessToken(this)) {
			return Promise.resolve(this);
		}
	}
	
	return this.getAccessToken()
		.then(function(data){
			try {
				// data 就是获取到的 access_token 相关信息
				/*
				{
					access_token:
					expire_in:
					...
				}
				*/
				// 验证是否合法
				var data = JSON.parse(data);
			} catch (e) {
				// 出错则重新获取
				return that.updateAccessToken();
			}
			// 不合法则重新获取
			if (that.isValidAccessToken(data)) {
				// 数据继续传递
				// return，后面的 then 才能拿到 data
				return Promise.resolve(data);
			} else {
				// 重新获取
				return that.updateAccessToken();
			}
		})
		.then(function(data){
			// 这里的 data 只能是合法的票据，因为不合法的话不会执行到这里
			// that.access_token = data.access_token;
			// that.expires_in = data.expires_in;
			that.saveAccessToken(data);
			
			return Promise.resolve(data);
		});
};

/**
 * 判断 access_token 是否有效（是否在有效期内）
 * @param data json对象
 * @returns {boolean}
 */
Wechat.prototype.isValidAccessToken = function(data){
	if (!data || !data.access_token || !data.expires_in) {
		return false;
	}

	var access_token = data.access_token;
	var expires_in = data.expires_in;
	var now_time = (new Date()).getTime();

	if (now_time < expires_in) {
		return true;
	} else {
		return false;
	}
};

/**
 * 更新 access_token
 * 处理之后，data.expire_in 不是7200，而是某个时间点。
 */
Wechat.prototype.updateAccessToken = function(){
	var appID = this.appID;
	var appSecret = this.appSecret;
	var url = api.accessToken + '&appid='+ appID +'&secret=' + appSecret;

	return new Promise(function(resolve, reject){
		// request 本身没有使用 Promise 来封装，需要我们自己处理，才能在后面使用 then 方法。
		request({url: url, json: true}).then(function(response){
			var data = response[1];
			var now_time = (new Date()).getTime();
			// 提前20 秒获取 access_token
			var expires_in = now_time + (data.expires_in - 20) * 1000;
			data.expires_in = expires_in;
			resolve(data); // 标识 fulfiled，并返回
		});
	});	
};

/**
 * 上传素材（包含临时素材和永久素材）
 * material 如果上传的是图片、视频、音频等文件类型的素材，则是文件的路径；如果上传的是图文素材，则是一个数组。
 */
Wechat.prototype.uploadMaterial = function(type, material, permanent){
	var that = this;
	var form = {};
	// 默认上传的是临时素材
	// 如果是永久素材，根据第三个参数，把该变量的值覆盖
	var uploadUrl = api.temporary.upload;
	if (permanent) {
		uploadUrl = api.permanent.upload;
		// 让变量 form 对象继承自 permanent对象。来自库：lodash。
		_.extend(form, permanent);
	}
	
	// 上传永久图文素材中的图片素材，获取图片的url
	if (type === 'pic') {
		uploadUrl = api.permanent.uploadNewsPic;
	}
	
	// 上传永久图文素材
	if (type === 'news') {
		uploadUrl = api.permanent.uploadNews;
		form = material;
	} else {
		form.media = fs.createReadStream(material);
	}

	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = uploadUrl + 'access_token='+ data.access_token;
				
				// 如果不是永久素材
				if (!permanent) {
					url += '&type=' + type;
				} else {
					form.access_token = data.access_token;
				}
				
				var options = {
					method: 'POST',
					url: url,
					json: true
				};
				
				// 图文
				if (type === 'news') {
					options.body = form;
				} else {
					options.formData = form;
				}
				
				// request 本身没有使用 Promise 来封装，需要我们自己处理，才能在后面使用 then 方法。
				// request({method: 'POST', url: url, formData: form, json: true})
				request(options)
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Upload material fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 下载素材（包含临时素材和永久素材）
 * material 如果上传的是图片、视频、音频等文件类型的素材，则是文件的路径；如果上传的是图文素材，则是一个数组。
 */
Wechat.prototype.fetchMaterial = function(type, mediaId, permanent){
	var that = this;
	// 默认下载的是临时素材
	// 如果是永久素材，根据第三个参数，把该变量的值覆盖
	var fetchUrl = api.temporary.fetch;
	if (permanent) {
		fetchUrl = api.permanent.fetch;
	}

	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = fetchUrl + 'access_token='+ data.access_token;
				
				var form = {};
				var options = {method: 'POST', url: url, json: true};
				
				if (permanent) {
					form.media_id = mediaId;
					form.access_token = data.access_token;
					options.body = form;
				} else {
					if (type == 'video') {
						url = url.replace('https:// ', 'http:// ');
					}
					url += '&media_id=' + mediaId;
				}
				
				// 图文和视频
				if ((type == 'news') || (type == 'video')) {
					request(options)
						.then(function(response){
							var _data = response[1];
							if (_data) {
								resolve(_data); // 标识 fulfiled，并返回
							} else {
								throw new Error('Fetch material fail');
							}			
						})
						.catch(function(err){
							reject(err);
						});
				} else {
					// 其他类型的素材，返回内容，可以直接输入到一个文件内保存
					resolve(url);
				}				
			});
	});	
};

/**
 * 删除永久素材（临时素材只会保留3天，不需要删除）
 */
Wechat.prototype.deleteMaterial = function(mediaId){
	var that = this;
	var form = {
		media_id: mediaId
	};
	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.permanent.del + 'access_token='+ data.access_token;				
				request({method: 'POST', url: url, body: form, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Delete material fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 修改永久素材（临时素材只会保留3天，不需要修改）
 */
Wechat.prototype.updateMaterial = function(mediaId, news){
	var that = this;
	var form = {
		media_id: mediaId
	};
	
	_.extend(form, news);
	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.permanent.update + 'access_token='+ data.access_token;				
				request({method: 'POST', url: url, body: form, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Delete material fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取永久素材数量
 */
Wechat.prototype.countMaterial = function(){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.permanent.count + 'access_token='+ data.access_token;				
				request({url: url, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Count material fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取永久素材列表
 */
Wechat.prototype.batchMaterial = function(options){
	var that = this;
	
	options.type = options.type || 'image';
	options.offset = options.offset || 0;
	options.count = options.count || 1;
	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.permanent.batch + 'access_token='+ data.access_token;				
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Batch material fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 创建分组
 */
Wechat.prototype.createGroup = function(name){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.group.create + 'access_token='+ data.access_token;
				var	options = {group: {name: name}};
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Create group fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取分组
 */
Wechat.prototype.fetchGroup = function(){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.group.fetch + 'access_token='+ data.access_token;
				request({url: url, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Fetch group fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取用户属于哪个分组
 */
Wechat.prototype.checkGroup = function(openid){
	var that = this;
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.group.check + 'access_token='+ data.access_token;
				var options = {openid: openid};
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Check group fail');
						}
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 修改分组（不是修改某个人所在的分组，只是修改分组名称）
 */
Wechat.prototype.updateGroup = function(gid, newname){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.group.update + 'access_token='+ data.access_token;
				var options = {group: {
					id: gid,
					name: newname
				}};
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Update group fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 移动用户所在分组
 */
Wechat.prototype.moveGroup = function(openid, gid){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.group.move + 'access_token='+ data.access_token;
				var options = {
					openid: openid,
					to_groupid: gid
				};
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Move group fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 批量移动用户所在分组
 */
Wechat.prototype.batchmoveGroup = function(openidList, gid){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.group.batchmove + 'access_token='+ data.access_token;
				var options = {
					openid_list: openidList,
					to_groupid: gid
				};
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Batchmove group fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 单个移动和批量移动用户所在分组的合并
 * openid 可能是一个id或者是id的数组
 */
Wechat.prototype.moveOrBatchmoveGroup = function(openid, gid){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var options = {to_groupid: gid};
				// 默认单个的url
				var url = api.group.move + 'access_token='+ data.access_token;
				// 批量
				if (_.isArray(openid)) {
					url = api.group.batchmove + 'access_token='+ data.access_token;
					options.openid_list = openid;
				} else {
					// 单个
					options.openid = openid;
				}
				
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('MoveOrBatchmoveGroup group fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 删除分组
 */
Wechat.prototype.deleteGroup = function(gid){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.group.del + 'access_token='+ data.access_token;
				var options = {
					group: {id: gid}
				};
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Delete group fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 设置用户备注名称
 */
Wechat.prototype.remarkUser = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.user.remark + 'access_token='+ data.access_token;
				var options = {
					openid: pdata.openid,
					remark: pdata.remark
				};
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Remark user fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取单个用户信息
 * pdata => {openid: 'xxx'[, lang: 'zh_CN'||'en']}
 * en 版本：得到的结果中 country是China，province是Zhejiang，city是Hangzhou。
 */
Wechat.prototype.fetchUser = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.user.fetch + 'access_token='+ data.access_token + '&openid=' + pdata.openid + '&lang=' + (pdata.lang || 'zh_CN');
				request({url: url, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Fetch user fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 批量获取用户信息
 * 参数 pdata 是openid 的数组 [openid1, openid2, ...]，或者 [{openid: 'xxx'[, lang: 'zh_CN'||'en']}]
 */
Wechat.prototype.batchFetchUser = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.user.batchFetch + 'access_token='+ data.access_token;
				var items = [];
				pdata.forEach(function(item){
					if(_.isString(item)) {
						items.push({
							openid: item,
							lang: 'zh_CN'
						});
					} else if (_.isObject(item)) {
						items.push({
							openid: item.openid,
							lang: item.lang || 'zh_CN'
						});
					}					
				});
				var options = {
					"user_list": items
				};
				request({method: 'POST', url: url, body: options, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('BatchFetch user fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取单个或者批量获取用户信息
 * 参数 pdata 
 * 1，单个 => {openid: 'xxx'[, lang: 'zh_CN'||'en']} 
 * 2，多个 => [openid1, openid2, ...]，或者 [{openid: 'xxx'[, lang: 'zh_CN'||'en']}]
 */
Wechat.prototype.fetchOrBatchFetchUser = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var form = {json: true};				
				if (_.isArray(pdata)){
					form.url = api.user.batchFetch + 'access_token='+ data.access_token;
					
					var items = [];
					pdata.forEach(function(item){
						if(_.isString(item)) {
							items.push({
								openid: item,
								lang: 'zh_CN'
							});
						} else if (_.isObject(item)) {
							items.push({
								openid: item.openid,
								lang: item.lang || 'zh_CN'
							});
						}					
					});
					var options = {
						"user_list": items
					};
					
					form.body = options;
					form.method = 'POST';
				} else {
					form.url = api.user.fetch + 'access_token='+ data.access_token + '&openid=' + pdata.openid + '&lang=' + (pdata.lang || 'zh_CN');
				}
				
				request(form)
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('BetchOrBatchFetch user fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取用户列表
 */
Wechat.prototype.listUser = function(nextOpenid){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.user.list + 'access_token='+ data.access_token;
				
				if(nextOpenid){
					url += '&next_openid=' + nextOpenid;
				}
				
				request({url: url, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('List user fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 根据分组进行群发（现在是根据标签进行群发了，接口一致）
 */
Wechat.prototype.massGroup = function(type, message, gid){
	var that = this;
	var msg = {
		filter: {},
		msgtype: type
	};
	
	msg[type] = message;
	/*
message 类似
图文mpnews
{"media_id":"123dsdajkasd231jhksad"}
图文消息的msg下还有一个属性："send_ignore_reprint":0，不知干嘛用的。

文本text
{"content":"CONTENT"},

语音/音频voice
{"media_id":"123dsdajkasd231jhksad"}

图片image（注意此处media_id需通过基础支持中的上传下载多媒体文件来得到）
{"media_id":"123dsdajkasd231jhksad"}

视频mpvideo（视频的media_id得到的比较特殊，看文档）
{"media_id":"IhdaAQXuvJtGzwwc0abfXnzeezfO0NgPK6AQYShD8RQYMTtfzbLdBIQkQziv2XJc"},

卡券消息wxcard（注意图文消息的media_id需要通过上述方法来得到）：
{"card_id":"123dsdajkasd231jhksad"}
	*/
	
	if (gid) {
		msg.filter = {
			is_to_all: false,
			group_id: gid
			// tag_id
		};
	} else {
		msg.filter.is_to_all = true;
	}
	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.mass.group + 'access_token='+ data.access_token;
				request({method: 'POST', url: url, body: msg, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Mass group fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 根据OpenID列表群发【订阅号不可用，服务号认证后可用】
 * openid 是 openid 的数组 [openid1, openid2, ...]
 */
Wechat.prototype.massOpenid = function(type, message, openid){
	var that = this;
	var msg = {
		touser: openid,
		msgtype: type
	};
	
	msg[type] = message;
	/*
message 类似
图文mpnews
{"media_id":"123dsdajkasd231jhksad"}
图文消息的msg下还有一个属性："send_ignore_reprint":0，不知干嘛用的。

文本text
{"content":"CONTENT"},

语音/音频voice
{"media_id":"123dsdajkasd231jhksad"}

图片image（注意此处media_id需通过基础支持中的上传下载多媒体文件来得到）
{"media_id":"123dsdajkasd231jhksad"}

视频mpvideo（视频的media_id得到的比较特殊，看文档）
{
	"media_id":"123dsdajkasd231jhksad",
	"title":"TITLE",
	"description":"DESCRIPTION"
}

卡券消息wxcard（注意图文消息的media_id需要通过上述方法来得到）：
{"card_id":"123dsdajkasd231jhksad"}
	*/
	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.mass.openid + 'access_token='+ data.access_token;
				request({method: 'POST', url: url, body: msg, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Mass openid fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 删除群发【订阅号与服务号认证后均可用】
 * {"msg_id": 30124, "article_idx": 2}
 * msg_id: 必须参数。发送出去的消息ID
 * article_idx: 可选参数。要删除的文章在图文消息中的位置，第一篇编号为1，该字段不填或填0会删除全部文章
 */
Wechat.prototype.deleteMass = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.mass.del + 'access_token='+ data.access_token;
				request({method: 'POST', url: url, body: pdata, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Mass delete fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 预览接口【订阅号与服务号认证后均可用】
 * openid 是一个字符串
 */
Wechat.prototype.previewMass = function(type, message, openid){
	var that = this;
	var msg = {
		touser: openid,
		msgtype: type
	};
	
	msg[type] = message;
	/*
message 类似
图文mpnews
{"media_id":"123dsdajkasd231jhksad"}
图文消息的msg下还有一个属性："send_ignore_reprint":0，不知干嘛用的。

文本text
{"content":"CONTENT"},

语音/音频voice
{"media_id":"123dsdajkasd231jhksad"}

图片image（注意此处media_id需通过基础支持中的上传下载多媒体文件来得到）
{"media_id":"123dsdajkasd231jhksad"}

视频mpvideo（视频的media_id得到的比较特殊，看文档）
{"media_id":"IhdaAQXuvJtGzwwc0abfXnzeezfO0NgPK6AQYShD8RQYMTtfzbLdBIQkQziv2XJc"}

卡券消息wxcard（注意图文消息的media_id需要通过上述方法来得到）：
{"card_id":"123dsdajkasd231jhksad",
"card_ext":"{\"code\":\"\",\"openid\":\"\",\"timestamp\":\"1402057159\",\"signature\":\"017bb17407c8e0058a66d72dcc61632b70f511ad\"}"
}
	*/	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.mass.preview + 'access_token='+ data.access_token;
				request({method: 'POST', url: url, body: pdata, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Mass preview fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 查询群发消息发送状态【订阅号与服务号认证后均可用】
 * message 是 {"msg_id": "201053012"}
 */
Wechat.prototype.checkMass = function(message){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.mass.check + 'access_token='+ data.access_token;
				request({method: 'POST', url: url, body: message, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Mass check fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 创建菜单
 */
Wechat.prototype.createMenu = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.menu.create + 'access_token='+ data.access_token;
				request({method: 'POST', url: url, body: pdata, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Create menu fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取菜单【在设置了个性化菜单后，使用本自定义菜单查询接口可以获取默认菜单和全部个性化菜单信息。】
 */
Wechat.prototype.fetchMenu = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.menu.fetch + 'access_token='+ data.access_token;
				request({url: url, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Fetch menu fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 自定义菜单删除接口【在个性化菜单时，调用此接口会删除默认菜单及全部个性化菜单。】
 */
Wechat.prototype.deleteMenu = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.menu.del + 'access_token='+ data.access_token;
				request({url: url, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Delete menu fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取自定义菜单配置接口
 * 【本接口将会提供公众号当前使用的自定义菜单的配置，如果公众号是通过API调用设置的菜单，
 * 则返回菜单的开发配置，而如果公众号是在公众平台官网通过网站功能发布菜单，则本接口返回运营者设置的菜单配置。】
 */
Wechat.prototype.currentMenu = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.menu.current + 'access_token='+ data.access_token;
				request({url: url, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Current menu fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 创建二维码（获取ticket，用ticket调用下面的接口 showQrcode 来获取二维码图片）
 */
Wechat.prototype.createQrcode = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.qrcode.create + 'access_token='+ data.access_token;
				request({method: 'POST', url: url, body: pdata, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Create qrcode fail');
						}			
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 获取二维码图片
 */
Wechat.prototype.showQrcode = function(ticket){
	return api.qrcode.show + 'ticket='+ encodedURIComponent(ticket);
};

/**
 * 创建短链接
 */
Wechat.prototype.createShorturl = function(action, url){
	var action = action || 'long2short';
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.shortUrl.create + 'access_token='+ data.access_token;
				var form = {
					action: urlType,
					long_url: url
				};
				request({method: 'POST', url: url, body: form, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Create shorturl fail');
						}
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 语义接口
 */
Wechat.prototype.semantic = function(pdata){
	var that = this;	
	return new Promise(function(resolve, reject){
		that.fetchAccessToken()
			.then(function(data){
				var url = api.semanticUrl + 'access_token='+ data.access_token;
				pdata.appid = data.appID;
				request({method: 'POST', url: url, body: pdata, json: true})
					.then(function(response){
						var _data = response[1];
						if (_data) {
							resolve(_data); // 标识 fulfiled，并返回
						} else {
							throw new Error('Semantic fail');
						}
					})
					.catch(function(err){
						reject(err);
					});
			});
	});	
};

/**
 * 针对 signature.js 中的 wechat.reply.call(this);
 * 因为回复消息，应该是WeChat对象的功能。
 * 但是，微信服务器推送来的xml消息被挂载到生成器函数的 this 上了。
 * 所以使用 apply来执行回复消息。
 */
Wechat.prototype.reply = function(){
	// 这里的 this 是生成器函数里的 this，已经被挂载了 weixin 属性，也能使用 status、type等属性来赋值然后进行发送给微信服务器
	var content = this.body; // 回复的内容，就是那些要返回给用户的 xml 字符串需要的参数
	var message = this.weixin; // 从微信服务器推送来的 xml 字符串（已经被转为 json 了）
	var xml = util.tpl(content, message); // 从收到的 xml 字符串提取 from、to等，组装成 xml 字符串赋值给 body。

	this.status = 200;
	this.type = 'application/xml';
	this.body = xml;
};

module.exports = Wechat;